/* eslint-disable
    node/handle-callback-err,
    max-len,
    new-cap,
    no-return-assign,
    no-unused-vars,
*/
// TODO: This file was created by bulk-decaffeinate.
// Fix any style issues and re-enable lint.
/*
 * decaffeinate suggestions:
 * DS101: Remove unnecessary use of Array.from
 * DS102: Remove unnecessary code created because of implicit returns
 * DS103: Rewrite code to no longer use __guard__
 * DS205: Consider reworking code to avoid use of IIFEs
 * DS206: Consider reworking classes to avoid initClass
 * DS207: Consider shorter variations of null checks
 * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
 */
import App from '../../../base'
import PDFJS from './pdfJsLoader'
import { captureMessage } from '../../../infrastructure/error-reporter'

export default App.factory('PDFRenderer', function(
  $timeout,
  pdfAnnotations,
  pdfTextLayer,
  pdfSpinner
) {
  let PDFRenderer
  return (PDFRenderer = (function() {
    PDFRenderer = class PDFRenderer {
      static initClass() {
        this.prototype.JOB_QUEUE_INTERVAL = 25
        this.prototype.PAGE_LOAD_TIMEOUT = 60 * 1000
        this.prototype.INDICATOR_DELAY1 = 100 // time to delay before showing the indicator
        this.prototype.INDICATOR_DELAY2 = 250 // time until the indicator starts animating
        this.prototype.TEXTLAYER_TIMEOUT = 100
      }

      constructor(url, options) {
        // set up external character mappings - needed for Japanese etc
        this.url = url
        this.options = options

        this.scale = this.options.scale || 1

        let disableFontFace
        if (
          __guard__(
            window.location != null ? window.location.search : undefined,
            x => x.indexOf('disable-font-face=true')
          ) >= 0
        ) {
          disableFontFace = true
        } else {
          disableFontFace = false
        }
        this.pdfjs = PDFJS.getDocument({
          url: this.url,
          cMapUrl: window.pdfCMapsPath,
          cMapPacked: true,
          disableFontFace,
          // Enable fetching with Range headers to restrict individual
          // requests to 128kb.
          // To do this correctly we must:
          // a) disable auto-fetching of the whole file upfront
          // b) disable streaming (which in this context means streaming of
          // the response into memory). This isn't supported when using
          // Range headers, but shouldn't be a problem since we are already
          // limiting individual response size through chunked range
          // requests
          rangeChunkSize: 128 * 1024,
          disableAutoFetch: !!this.options.disableAutoFetch,
          disableStream: !!this.options.disableAutoFetch
        })
        this.pdfjs.onProgress = this.options.progressCallback
        this.document = this.pdfjs
        this.navigateFn = this.options.navigateFn
        this.spinner = new pdfSpinner()
        this.resetState()
        this.document.promise.then(pdfDocument => {
          return pdfDocument.getDownloadInfo().then(() => {
            return this.options.loadedCallback()
          })
        })
        this.errorCallback = this.options.errorCallback
        this.pageSizeChangeCallback = this.options.pageSizeChangeCallback
        this.pdfjs.promise.catch(exception => {
          // error getting document
          return this.errorCallback(exception)
        })
      }

      resetState() {
        this.renderQueue = []
        if (this.queueTimer != null) {
          clearTimeout(this.queueTimer)
        }
        // clear any existing timers, render tasks
        for (let timer of Array.from(this.spinTimer || [])) {
          clearTimeout(timer)
        }
        for (let page of Array.from(this.pageState || [])) {
          __guard__(page != null ? page.loadTask : undefined, x => x.cancel())
          __guard__(page != null ? page.renderTask : undefined, x1 =>
            x1.cancel()
          )
        }
        // initialise/reset the state
        this.pageState = []
        this.spinTimer = [] // timers for starting the spinners (to avoid jitter)
        this.spinTimerDone = [] // array of pages where the spinner has activated
        return (this.jobs = 0)
      }

      getNumPages() {
        return this.document.promise.then(pdfDocument => pdfDocument.numPages)
      }

      getPage(pageNum) {
        return this.document.promise.then(pdfDocument =>
          pdfDocument.getPage(pageNum)
        )
      }

      getPdfViewport(pageNum, scale) {
        if (scale == null) {
          ;({ scale } = this)
        }
        return this.document.promise.then(pdfDocument => {
          return pdfDocument.getPage(pageNum).then(
            function(page) {
              let viewport
              return (viewport = page.getViewport({ scale: scale }))
            },
            error => {
              return typeof this.errorCallback === 'function'
                ? this.errorCallback(error)
                : undefined
            }
          )
        })
      }

      getDestinations() {
        return this.document.promise.then(pdfDocument =>
          pdfDocument.getDestinations()
        )
      }

      getDestination(dest) {
        return this.document.promise.then(
          pdfDocument => pdfDocument.getDestination(dest),
          error => {
            return typeof this.errorCallback === 'function'
              ? this.errorCallback(error)
              : undefined
          }
        )
      }

      getPageIndex(ref) {
        return this.document.promise.then(pdfDocument => {
          return pdfDocument.getPageIndex(ref).then(
            idx => idx,
            error => {
              return typeof this.errorCallback === 'function'
                ? this.errorCallback(error)
                : undefined
            }
          )
        })
      }

      getScale() {
        return this.scale
      }

      setScale(scale) {
        this.scale = scale
        return this.resetState()
      }

      triggerRenderQueue(interval) {
        if (interval == null) {
          interval = this.JOB_QUEUE_INTERVAL
        }
        if (this.queueTimer != null) {
          clearTimeout(this.queueTimer)
        }
        return (this.queueTimer = setTimeout(() => {
          this.queueTimer = null
          return this.processRenderQueue()
        }, interval))
      }

      removeCompletedJob(pagenum) {
        this.jobs = this.jobs - 1
        return this.triggerRenderQueue(0)
      }

      renderPages(pages) {
        if (this.shuttingDown) {
          return
        }
        this.renderQueue = Array.from(pages).map(page => ({
          element: page.elementChildren,
          pagenum: page.pageNum
        }))
        return this.triggerRenderQueue()
      }

      renderPage(page) {
        if (this.shuttingDown) {
          return
        }
        const current = {
          element: page.elementChildren,
          pagenum: page.pageNum
        }
        this.renderQueue.push(current)
        return this.processRenderQueue()
      }

      getPageDetails(page) {
        return [page.element.canvas, page.pagenum]
      }

      // handle the loading indicators for each page

      startIndicators() {
        // make an array of the pages in the queue
        this.queuedPages = []
        for (var page of Array.from(this.renderQueue)) {
          this.queuedPages[page.pagenum] = true
        }
        // clear any unfinished spinner timers on pages that aren't in the queue any more
        for (let pagenum in this.spinTimer) {
          if (!this.queuedPages[pagenum]) {
            clearTimeout(this.spinTimer[pagenum])
            delete this.spinTimer[pagenum]
          }
        }
        // add indicators for any new pages in the current queue
        return (() => {
          const result = []
          for (page of Array.from(this.renderQueue)) {
            if (
              !this.spinTimer[page.pagenum] &&
              !this.spinTimerDone[page.pagenum]
            ) {
              result.push(this.startIndicator(page))
            }
          }
          return result
        })()
      }

      startIndicator(page) {
        const [canvas, pagenum] = Array.from(this.getPageDetails(page))
        canvas.addClass('pdfng-loading')
        return (this.spinTimer[pagenum] = setTimeout(() => {
          for (let queuedPage of Array.from(this.renderQueue)) {
            if (pagenum === queuedPage.pagenum) {
              this.spinner.add(canvas, { static: true })
              this.spinTimerDone[pagenum] = true
              break
            }
          }
          return delete this.spinTimer[pagenum]
        }, this.INDICATOR_DELAY1))
      }

      updateIndicator(page) {
        const [canvas, pagenum] = Array.from(this.getPageDetails(page))
        // did the spinner insert itself already?
        if (this.spinTimerDone[pagenum]) {
          return (this.spinTimer[pagenum] = setTimeout(() => {
            this.spinner.start(canvas)
            return delete this.spinTimer[pagenum]
          }, this.INDICATOR_DELAY2))
        } else {
          // stop the existing spin timer
          clearTimeout(this.spinTimer[pagenum])
          // start a new one which will also start spinning
          return (this.spinTimer[pagenum] = setTimeout(() => {
            this.spinner.add(canvas, { static: true })
            this.spinTimerDone[pagenum] = true
            return (this.spinTimer[pagenum] = setTimeout(() => {
              this.spinner.start(canvas)
              return delete this.spinTimer[pagenum]
            }, this.INDICATOR_DELAY2))
          }, this.INDICATOR_DELAY1))
        }
      }

      clearIndicator(page) {
        const [canvas, pagenum] = Array.from(this.getPageDetails(page))
        this.spinner.stop(canvas)
        clearTimeout(this.spinTimer[pagenum])
        delete this.spinTimer[pagenum]
        return (this.spinTimerDone[pagenum] = true)
      }

      // handle the queue of pages to be rendered

      processRenderQueue() {
        let pageState
        if (this.shuttingDown) {
          return
        }
        // mark all pages in the queue as loading
        this.startIndicators()
        // bail out if there is already a render job running
        if (this.jobs > 0) {
          return
        }
        // take the first page in the queue
        let page = this.renderQueue.shift()
        // check if it is in action already
        while (page != null && this.pageState[page.pagenum] != null) {
          page = this.renderQueue.shift()
        }
        if (page == null) {
          return
        }
        const [element, pagenum] = Array.from([page.element, page.pagenum])
        this.jobs = this.jobs + 1

        // update the spinner to make it spinning (signifies loading has begun)
        this.updateIndicator(page)

        let timedOut = false
        const timer = $timeout(() => {
          // page load timed out
          if (loadTask.cancelled) {
            return
          } // return from cancelled page load
          captureMessage(
            `pdfng page load timed out after ${this.PAGE_LOAD_TIMEOUT}ms`
          )

          timedOut = true
          this.clearIndicator(page)
          // @jobs = @jobs - 1
          // @triggerRenderQueue(0)
          return typeof this.errorCallback === 'function'
            ? this.errorCallback('timeout')
            : undefined
        }, this.PAGE_LOAD_TIMEOUT)

        var loadTask = this.getPage(pagenum)

        loadTask.cancel = function() {
          return (this.cancelled = true)
        }

        this.pageState[pagenum] = pageState = { loadTask }

        return loadTask
          .then(pageObject => {
            // page load success
            $timeout.cancel(timer)
            if (loadTask.cancelled) {
              return
            } // return from cancelled page load
            pageState.renderTask = this.doRender(element, pagenum, pageObject)
            return pageState.renderTask.promise.then(
              () => {
                // render task success
                this.clearIndicator(page)
                pageState.complete = true
                delete pageState.renderTask
                return this.removeCompletedJob(pagenum)
              },
              () => {
                // render task failed
                // could display an error icon
                pageState.complete = false
                delete pageState.renderTask
                return this.removeCompletedJob(pagenum)
              }
            )
          })
          .catch(error => {
            // page load error
            $timeout.cancel(timer)
            return this.clearIndicator(page)
          })
      }

      doRender(element, pagenum, page) {
        const self = this
        const { scale } = this

        if (scale == null) {
          // scale is undefined, returning
          return
        }

        const canvas = $('<canvas class="pdf-canvas pdfng-rendering"></canvas>')
        // In Windows+IE we must have the canvas in the DOM during
        // rendering to see the fonts defined in the DOM. If we try to
        // render 'offscreen' then all the text will be sans-serif.
        // Previously we rendered offscreen and added in the canvas
        // when rendering was complete.
        element.canvas.replaceWith(canvas)

        const viewport = page.getViewport({ scale: scale })

        const devicePixelRatio = window.devicePixelRatio || 1

        const ctx = canvas[0].getContext('2d')
        const backingStoreRatio =
          ctx.webkitBackingStorePixelRatio ||
          ctx.mozBackingStorePixelRatio ||
          ctx.msBackingStorePixelRatio ||
          ctx.oBackingStorePixelRatio ||
          ctx.backingStorePixelRatio ||
          1
        const pixelRatio = devicePixelRatio / backingStoreRatio

        const scaledWidth = (Math.floor(viewport.width) * pixelRatio) | 0
        const scaledHeight = (Math.floor(viewport.height) * pixelRatio) | 0

        const newWidth = Math.floor(viewport.width)
        const newHeight = Math.floor(viewport.height)

        canvas[0].height = scaledHeight
        canvas[0].width = scaledWidth

        canvas.height(newHeight + 'px')
        canvas.width(newWidth + 'px')

        const oldHeight = element.canvas.height()
        const oldWidth = element.canvas.width()
        if (newHeight !== oldHeight || newWidth !== oldWidth) {
          element.canvas.height(newHeight + 'px')
          element.canvas.width(newWidth + 'px')
          element.container.height(newHeight + 'px')
          element.container.width(newWidth + 'px')
          if (typeof this.pageSizeChangeCallback === 'function') {
            this.pageSizeChangeCallback(pagenum, newHeight - oldHeight)
          }
        }

        const textLayer = new pdfTextLayer({
          textLayerDiv: element.text[0],
          viewport,
          renderer: PDFJS.renderTextLayer
        })

        const annotationsLayer = new pdfAnnotations({
          annotations: element.annotations[0],
          viewport,
          navigateFn: this.navigateFn
        })

        const result = page.render({
          canvasContext: ctx,
          viewport,
          transform: [pixelRatio, 0, 0, pixelRatio, 0, 0]
        })

        const textLayerTimeout = this.TEXTLAYER_TIMEOUT

        result.promise
          .then(function() {
            // page render success
            canvas.removeClass('pdfng-rendering')
            page.getTextContent({ normalizeWhitespace: true }).then(
              function(textContent) {
                textLayer.setTextContent(textContent)
                return textLayer.render(textLayerTimeout)
              },
              error =>
                typeof self.errorCallback === 'function'
                  ? self.errorCallback(error)
                  : undefined
            )
            return page.getAnnotations().then(
              annotations => annotationsLayer.setAnnotations(annotations),
              error =>
                typeof self.errorCallback === 'function'
                  ? self.errorCallback(error)
                  : undefined
            )
          })
          .catch(function(error) {
            // page render failed
            if (error.name === 'RenderingCancelledException') {
              // do nothing when cancelled
            } else {
              return typeof self.errorCallback === 'function'
                ? self.errorCallback(error)
                : undefined
            }
          })

        return result
      }

      destroy() {
        this.shuttingDown = true
        this.resetState()
        return this.pdfjs.promise.then(function(document) {
          document.cleanup()
          return document.destroy()
        })
      }
    }
    PDFRenderer.initClass()
    return PDFRenderer
  })())
})

function __guard__(value, transform) {
  return typeof value !== 'undefined' && value !== null
    ? transform(value)
    : undefined
}
function __guardMethod__(obj, methodName, transform) {
  if (
    typeof obj !== 'undefined' &&
    obj !== null &&
    typeof obj[methodName] === 'function'
  ) {
    return transform(obj, methodName)
  } else {
    return undefined
  }
}
